From b2180648656e9628ce7c156f87fc45e051072e25 Mon Sep 17 00:00:00 2001 From: Andrew Garrett Date: Mon, 16 Jul 2012 16:04:20 -0700 Subject: [PATCH] Function for "pretty timestamps" that are human readable and understandable. Uses one of the following formats: - Just now - 1 minute ago - 35 minutes ago - 13:04 - Yesterday at 13:04 - Wednesday at 13:04 - July 16 - July 16, 2012 Change-Id: I53dcf54763c68f15fc4f59b2668001b0cf84adf3 --- languages/Language.php | 141 +++++++++++++++++++++++ languages/messages/MessagesEn.php | 20 ++++ languages/messages/MessagesQqq.php | 15 +++ maintenance/language/messages.inc | 12 ++ tests/phpunit/languages/LanguageTest.php | 125 +++++++++++++++++++- 5 files changed, 312 insertions(+), 1 deletion(-) diff --git a/languages/Language.php b/languages/Language.php index 0ca21bd5c0..dfa8e2cd30 100644 --- a/languages/Language.php +++ b/languages/Language.php @@ -154,6 +154,10 @@ class Language { 'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12' ); + // For pretty timestamps + // Cutoff for specifying "weekday at XX:XX" format + protected $mWeekdayAtCutoff = 432000; // 5 days + /** * @since 1.20 * @var array @@ -1844,6 +1848,7 @@ class Language { * @param $usePrefs Mixed: if true, the user's preference is used * if false, the site/language default is used * if int/string, assumed to be a format. + * if User object, assumed to be a User to get preference from * @return string */ function dateFormat( $usePrefs = true ) { @@ -1855,6 +1860,8 @@ class Language { } else { $datePreference = (string)User::getDefaultOption( 'date' ); } + } elseif ( $usePrefs instanceof User ) { + $datePreference = $usePrefs->getDatePreference(); } else { $datePreference = (string)$usePrefs; } @@ -1881,11 +1888,17 @@ class Language { $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" ); } else { $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" ); + + if ( $type === 'shortdate' && is_null( $df ) ) { + $df = $this->getDateFormatString( 'date', $pref ); + } + if ( is_null( $df ) ) { $pref = $this->getDefaultDateFormat(); $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" ); } } + $this->dateFormatStrings[$type][$pref] = $df; } return $this->dateFormatStrings[$type][$pref]; @@ -2114,6 +2127,134 @@ class Language { return $this->internalUserTimeAndDate( 'both', $ts, $user, $options ); } + /** + * Formats a timestamp in a pretty, human-readable format. + * Instead of "13:04, 16 July 2012", we have: + * - Just now + * - 35 minutes ago + * - At 13:04 + * - Yesterday at 13:04 + * - Wednesday at 13:04 + * - July 16, 13:04 + * - July 16 2012 at 13:04 + * + * @todo Port to JavaScript + * + * @param $ts Mixed: the time format which needs to be turned into a + * date('YmdHis') format with wfTimestamp(TS_MW,$ts) + * @param $relativeTo Mixed: The timestamp to use as "now" + * @param $user User: The user to format for (needed for timezone information) + * @since 1.20 + * @return string Formatted timestamp + */ + public function prettyTimestamp( $timestamp, $relativeTo = false, $user ) { + // Parameter defaults + if ( $relativeTo === false ) { + $relativeTo = wfTimestampNow(); + } + + // Normalise input to UNIX time + $relativeTo = wfTimestamp( TS_UNIX, $relativeTo ); + $timestamp = wfTimestamp( TS_UNIX, $timestamp ); + $timeAgo = $relativeTo - $timestamp; + + $adjustedRelativeTo = $this->userAdjust( wfTimestamp( TS_MW, $relativeTo ), $user->getOption('timecorrection') ); + $adjustedRelativeTo = wfTimestamp( TS_UNIX, $adjustedRelativeTo ); + $relativeToYear = gmdate( 'Y', $adjustedRelativeTo ); + + $adjustedTimestamp = $this->userAdjust( wfTimestamp( TS_MW, $timestamp ), $user->getOption('timecorrection') ); + $adjustedTimestamp = wfTimestamp( TS_UNIX, $adjustedTimestamp ); + $timestampYear = gmdate( 'Y', $adjustedTimestamp ); + + if ( $timeAgo < 0 ) { + throw new MWException( "Future timestamps not currently supported" ); + } elseif ( $timeAgo < 30 ) { + return wfMessage( 'just-now' ) + ->inLanguage( $this ) + ->text(); + } elseif ( $timeAgo < 5400 ) { + // Less than 90 minutes ago. Return number of hours, minutes or seconds ago. + return $this->formatRelativeTime( $timeAgo ); + } elseif ( // Same day + intval( $adjustedRelativeTo / (24*60*60) ) === + intval( $adjustedTimestamp / (24*60*60) ) + ) { + // Today at XX:XX + $time = $this->time( $adjustedTimestamp ); + return wfMessage( 'today-at' ) + ->inLanguage( $this ) + ->params( $time ) + ->text(); + } elseif ( // Previous day + intval( $adjustedRelativeTo / (24*60*60) ) === + ( intval( $adjustedTimestamp / (24*60*60) ) + 1 ) + ) { + // Yesterday at XX:XX + $time = $this->time( $adjustedTimestamp ); + + return wfMessage( 'yesterday-at' ) + ->inLanguage( $this ) + ->params( $time ) + ->text(); + } elseif ( $timeAgo < ( $this->mWeekdayAtCutoff ) ) { // Less than 5 days ago + // Xday at XX:XX + return $this->formatPastWeekTimestamp( $adjustedTimestamp, $adjustedRelativeTo ); + } elseif ( $relativeToYear == $timestampYear ) { + // XX XMonth + $df = $this->getDateFormatString( 'shortdate', $this->dateFormat( $user ) ); + $mwTimestamp = wfTimestamp( TS_MW, $timestamp ); + return $this->sprintfDate( $df, $mwTimestamp ); + } else { + // Full timestamp + $mwTimestamp = wfTimestamp( TS_MW, $timestamp ); + return $this->userDate( $mwTimestamp, $user ); + } + } + + /** + * For pretty timestamps: Formats the "X {hours,minutes,seconds} ago" message. + * + * @param $timeAgo The number of seconds ago the event occurred + * @return Formatted string + */ + protected function formatRelativeTime( $timeAgo ) { + $count = false; + if ( $timeAgo < 60 ) { + $unit = 'seconds'; + $count = $timeAgo; + } elseif ( $timeAgo < 3600 ) { + $unit = 'minutes'; + $count = intval( $timeAgo / 60 ); + } else { + $unit = 'hours'; + $count = intval( $timeAgo / 3600 ); + } + + return wfMessage( "{$unit}-ago" )->inLanguage( $this )->params( $count )->text(); + } + + /** + * For pretty timestamps: Formats the timestamp for events that occurred + * "in the last few days". + * (cutoff is configurable by the member variable $mWeekdayAtCutoff) + * + * @param $eventTimestamp The timestamp of the event, adjusted to + * the user's local timezone, in UNIX format (# of seconds since epoch) + * @param $relativeTo The timestamp to format relative to, adjusted to + * the user's local timezone, in UNIX format (# of seconds since epoch) + * @return String: The date formatted in a human-friendly way. + */ + protected function formatPastWeekTimestamp( $adjustedTimestamp, $relativeTo ) { + $day = date( 'w', $adjustedTimestamp ); + $weekday = self::$mWeekdayMsgs[$day]; + $time = $this->time( $adjustedTimestamp ); + + return wfMessage( "$weekday-at" ) + ->inLanguage( $this ) + ->params( $time ) + ->text(); + } + /** * @param $key string * @return array|null diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index f0444cbfe2..471653804c 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -162,18 +162,22 @@ $dateFormats = array( 'mdy time' => 'H:i', 'mdy date' => 'F j, Y', 'mdy both' => 'H:i, F j, Y', + 'mdy shortdate' => 'F j', 'dmy time' => 'H:i', 'dmy date' => 'j F Y', 'dmy both' => 'H:i, j F Y', + 'dmy shortdate' => 'j F', 'ymd time' => 'H:i', 'ymd date' => 'Y F j', 'ymd both' => 'H:i, Y F j', + 'ymd shortdate' => 'F j', 'ISO 8601 time' => 'xnH:xni:xns', 'ISO 8601 date' => 'xnY-xnm-xnd', 'ISO 8601 both' => 'xnY-xnm-xnd"T"xnH:xni:xns', + // 'ISO 8601 shortdate' => 'xnm-xnd', // This is just confusing ); /** @@ -3857,6 +3861,22 @@ By executing it, your system may be compromised.", 'days' => '{{PLURAL:$1|$1 day|$1 days}}', 'ago' => '$1 ago', +'hours-ago' => '$1 {{PLURAL:$1|hour|hours}} ago', +'minutes-ago' => '$1 {{PLURAL:$1|minute|minutes}} ago', +'seconds-ago' => '$1 {{PLURAL:$1|seconds|seconds}} ago', + +'monday-at' => 'Monday at $1', +'tuesday-at' => 'Tuesday at $1', +'wednesday-at' => 'Wednesday at $1', +'thursday-at' => 'Thursday at $1', +'friday-at' => 'Friday at $1', +'saturday-at' => 'Saturday at $1', +'sunday-at' => 'Sunday at $1', + +'today-at' => '$1', +'yesterday-at' => 'Yesterday at $1', +'just-now' => 'Just now', + # Bad image list 'bad_image_list' => 'The format is as follows: diff --git a/languages/messages/MessagesQqq.php b/languages/messages/MessagesQqq.php index c8c958bd3c..3544138ebe 100644 --- a/languages/messages/MessagesQqq.php +++ b/languages/messages/MessagesQqq.php @@ -3648,6 +3648,21 @@ Part of variable $1 in {{msg-mw|Ago}} *{{msg-mw|Hours}} *{{msg-mw|Days}}', +'hours-ago' => 'Phrase for indicating that something occurred a certain number of hours ago', +'minutes-ago' => 'Phrase for indicating that something occurred a certain number of minutes ago', +'seconds-ago' => 'Phrase for indicating that something occurred a certain number of seconds ago', + +'monday-at' => 'Phrase for indicating that something occurred at a particular time on the most recent Monday. $1 is the time.', +'tuesday-at' => 'Phrase for indicating that something occurred at a particular time on the most recent Tuesday. $1 is the time.', +'wednesday-at' => 'Phrase for indicating that something occurred at a particular time on the most recent Wednesday. $1 is the time.', +'thursday-at' => 'Phrase for indicating that something occurred at a particular time on the most recent Thursday. $1 is the time.', +'friday-at' => 'Phrase for indicating that something occurred at a particular time on the most recent Friday. $1 is the time.', +'saturday-at' => 'Phrase for indicating that something occurred at a particular time on the most recent Saturday. $1 is the time.', +'sunday-at' => 'Phrase for indicating that something occurred at a particular time on the most recent Sunday. $1 is the time.', +'today-at' => 'Phrase for indicating that something occurred at a particular time today. $1 is the time.', +'yesterday-at' => 'Phrase for indicating that something occurred at a particular time yesterday. $1 is the time.', +'just-now' => 'Phrase for indicating that something occurred very recently (for example, less than 30 seconds ago)', + # Bad image list 'bad_image_list' => 'This message only appears to guide administrators to add links with the right format. This will not appear anywhere else in MediaWiki.', diff --git a/maintenance/language/messages.inc b/maintenance/language/messages.inc index 8fa26f6382..e4472e44fb 100644 --- a/maintenance/language/messages.inc +++ b/maintenance/language/messages.inc @@ -137,6 +137,18 @@ $wgMessageStructure = array( 'nov', 'dec', ), + 'pretty-timestamps' => array( + 'monday-at', + 'tuesday-at', + 'wednesday-at', + 'thursday-at', + 'friday-at', + 'saturday-at', + 'sunday-at', + 'today-at', + 'yesterday-at', + 'just-now', + ), 'categorypages' => array( 'pagecategories', 'pagecategorieslink', diff --git a/tests/phpunit/languages/LanguageTest.php b/tests/phpunit/languages/LanguageTest.php index bfb45c75c6..5ca6a1f383 100644 --- a/tests/phpunit/languages/LanguageTest.php +++ b/tests/phpunit/languages/LanguageTest.php @@ -1090,5 +1090,128 @@ class LanguageTest extends MediaWikiTestCase { ) ), ); } -} + /** + * @test + * @dataProvider providePrettyTimestampTests + */ + public function testPrettyTimestamp( + $tsTime, // The timestamp to format + $currentTime, // The time to consider "now" + $timeCorrection, // The time offset to use + $dateFormat, // The date preference to use + $expectedOutput, // The expected output + $desc // Description + ) { + $user = new User; + $user->setOption( 'timecorrection', $timeCorrection ); + $user->setOption( 'date', $dateFormat ); + + $this->assertEquals( + $expectedOutput, + $this->lang->prettyTimestamp( $tsTime, $currentTime, $user ), + $desc + ); + } + + public function providePrettyTimestampTests() { + return array( + array( + '20111231170000', + '20120101000000', + 'Offset|0', + 'mdy', + 'Yesterday at 17:00', + '"Yesterday" across years', + ), + array( + '20120717190900', + '20120717190929', + 'Offset|0', + 'mdy', + 'Just now', + '"Just now"', + ), + array( + '20120717190900', + '20120717191530', + 'Offset|0', + 'mdy', + '6 minutes ago', + 'X minutes ago', + ), + array( + '20121006173100', + '20121006173200', + 'Offset|0', + 'mdy', + '1 minute ago', + '"1 minute ago"', + ), + array( + '20120617190900', + '20120717190900', + 'Offset|0', + 'mdy', + 'June 17', + 'Another month' + ), + array( + '19910130151500', + '20120716193700', + 'Offset|0', + 'mdy', + 'January 30, 1991', + 'Different year', + ), + array( + '20120101050000', + '20120101080000', + 'Offset|-360', + 'mdy', + 'Yesterday at 23:00', + '"Yesterday" across years with time correction', + ), + array( + '20120714184300', + '20120716184300', + 'Offset|-420', + 'mdy', + 'Saturday at 11:43', + 'Recent weekday with time correction', + ), + array( + '20120714184300', + '20120715040000', + 'Offset|-420', + 'mdy', + '11:43', + 'Today at another time with time correction', + ), + array( + '20120617190900', + '20120717190900', + 'Offset|0', + 'dmy', + '17 June', + 'Another month with dmy' + ), + array( + '20120617190900', + '20120717190900', + 'Offset|0', + 'ISO 8601', + '2012-06-17', + 'Another month with ISO-8601' + ), + array( + '19910130151500', + '20120716193700', + 'Offset|0', + 'ISO 8601', + '1991-01-30', + 'Different year with ISO-8601', + ), + ); + } +} -- 2.20.1